﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text;

namespace Bouyei.GeoCore.GeoParsers.Dbf
{
    public class DbfHeader 
    {
        public const int FieldNameMaxLength = 11;

        // Constant for the size of a record
        private const int FileDescriptorSize = 32;

        // type of the file, must be 03h
        private int _fileType = 0x03;

        // Date the file was last updated.
        private DateTime _updateDate;

        // Number of records in the datafile
        private int _numRecords;

        // Length of the header structure
        private int _headerLength;

        // Length of the records
        private int _recordLength;

        // Number of fields in the record.
        private int _numFields;

        /// <summary>
        /// The encoding
        /// </summary>
        private Encoding _encoding;

        // collection of header records.
        private DbfColumn[] fieldColumns;

        /// <summary>
        /// Initializes a new instance of the DbaseFileHeader class.
        /// </summary>
        public DbfHeader()
            : this(null) { }

        /// <summary>
        /// Initializes a new instance of the DbaseFileHeader class.
        /// </summary>
        /// <param name="encoding">The encoding to use for strings</param>
        public DbfHeader(Encoding encoding)
        {
            _encoding = encoding;
            fieldColumns = new DbfColumn[0];
        }

        /// <summary>
        /// Sets or returns the date this file was last updated.
        /// </summary>
        /// <returns></returns>
        public DateTime LastUpdateDate
        {
            get { return _updateDate; }
            set { _updateDate = value; }
        }

        /// <summary>
        /// Return the number of fields in the records.
        /// </summary>
        /// <returns></returns>
        public int NumFields
        {
            get { return _numFields; }
            set { _numFields = value; }
        }

        public Encoding Encoding
        {
            get { return _encoding; }
            set
            {
                _encoding = value;
            }
        }

        /// <summary>
        /// Return the number of records in the file.
        /// </summary>
        /// <returns></returns>
        public int NumRecords
        {
            get { return _numRecords; }
            set { _numRecords = value; }
        }

        /// <summary>
        /// Return the length of the records in bytes.
        /// </summary>
        /// <returns></returns>
        public int RecordLength => _recordLength;

        public DbfColumn[] Fields => fieldColumns;
        /// <summary>
        /// Return the length of the header.
        /// </summary>
        /// <returns></returns>
        public int HeaderLength => _headerLength;

        public void WriteHeader(BinaryWriter writer)
        {
            if (Encoding == null)  Encoding = Encoding.ASCII;

            // write the output file type.
            writer.Write((byte)_fileType);

            writer.Write((byte)(_updateDate.Year - 1900));
            writer.Write((byte)_updateDate.Month);
            writer.Write((byte)_updateDate.Day);

            // write the number of records in the datafile.
            writer.Write(_numRecords);

            // write the length of the header structure.
            writer.Write((short)_headerLength);

            // write the length of a record
            writer.Write((short)_recordLength);

            // write the reserved bytes in the header
            byte[] data = new byte[20];
            for (int i = 0; i < 20; i++)
                data[i] = 0;
            data[29 - 12] = GetLdidFromEncoding(_encoding);
            writer.Write(data);

            // write all of the header records
            //int tempOffset = 0;
            for (int i = 0; i < fieldColumns.Length; i++)
            {
                // write the field name
                string fieldName = fieldColumns[i].Name;

                // make sure the field name data length is not bigger than FieldNameMaxLength (11)
                while (_encoding.GetByteCount(fieldName) > FieldNameMaxLength)
                    fieldName = fieldName.Substring(0, fieldName.Length - 1);

                byte[] buffer = new byte[FieldNameMaxLength];
                byte[] bytes = _encoding.GetBytes(fieldName);
                Array.Copy(bytes, buffer, bytes.Length);
                writer.Write(buffer);

                // write the field type
                writer.Write(fieldColumns[i].DbaseType);

                // write the field data address, offset from the start of the record.
                writer.Write(0);
                //tempOffset += _fieldDescriptions[i].Length;

                // write the length of the field.
                writer.Write((byte)fieldColumns[i].Length);

                // write the decimal count.
                writer.Write((byte)fieldColumns[i].DecimalCount);

                // write the reserved bytes.
                for (int j = 0; j < 14; j++) writer.Write((byte)0);
            }

            // write the end of the field definitions marker
            writer.Write((byte)0x0D);
        }

        public void AddColumn(string fieldName, char fieldType, int fieldLength, int decimalCount)
        {
            if (Encoding == null)  Encoding = Encoding.ASCII;

            if (fieldLength <= 0) fieldLength = 1;
            int tempLength = 1;  // the length is used for the offset, and there is a * for deleted as the first byte
            var columns = new DbfColumn[fieldColumns.Length + 1];

            for (int i = 0; i < fieldColumns.Length; i++)
            {
                fieldColumns[i].DataAddress = tempLength;
                tempLength = tempLength + fieldColumns[i].Length;
                columns[i] = fieldColumns[i];
            }
            columns[fieldColumns.Length] = new DbfColumn();
            columns[fieldColumns.Length].Length = fieldLength;
            columns[fieldColumns.Length].DecimalCount = decimalCount;
            columns[fieldColumns.Length].DataAddress = tempLength;

            // set the field name
            string tempFieldName = fieldName;
            if (tempFieldName == null) tempFieldName = "NoName";
            if (tempFieldName.Length > FieldNameMaxLength)
            {
                string s = string.Format("FieldName {0} is longer than {1} characters", fieldName, FieldNameMaxLength);
                throw new ArgumentException(s);
            }
            columns[fieldColumns.Length].Name = tempFieldName;

            // the field type
            switch (fieldType)
            {
                case 'C':
                case 'c':
                    columns[fieldColumns.Length].DbaseType = 'C';
                    if (fieldLength > 254) throw new Exception("field length max length is 254");
                    break;
                case 'S':
                case 's':
                    columns[fieldColumns.Length].DbaseType = 'C';
                    if (fieldLength > 254) throw new Exception("field length max length is 254");
                    columns[fieldColumns.Length].Length = 8;
                    break;
                case 'D':
                case 'd':
                    columns[fieldColumns.Length].DbaseType = 'D';
                    if (fieldLength != 8) throw new Exception("date field length must length is 8");
                    columns[fieldColumns.Length].Length = 8;
                    break;
                case 'F':
                case 'f':
                    columns[fieldColumns.Length].DbaseType = 'F';
                    if (fieldLength > 20) throw new Exception("date field length  max length is 20");
                    break;
                case 'N':
                case 'n':
                    columns[fieldColumns.Length].DbaseType = 'N';
                    if (fieldLength > 18) throw new Exception("date field length  max length is 18");
                    if (decimalCount < 0)
                    {
                        columns[fieldColumns.Length].DecimalCount = 0;
                    }
                    if (decimalCount > fieldLength - 1)
                    {
                        columns[fieldColumns.Length].DecimalCount = fieldLength - 1;
                    }
                    break;
                case 'L':
                case 'l':
                    columns[fieldColumns.Length].DbaseType = 'L';
                    if (fieldLength != 1) throw new Exception("date field length  max is 1");
                    columns[fieldColumns.Length].Length = 1;
                    break;
                default:
                    throw new NotSupportedException("Unsupported field type " + fieldType + " For column " + fieldName);
            }
            // the length of a record
            tempLength = tempLength + columns[fieldColumns.Length].Length;

            // set the new fields.
            fieldColumns = columns;
            _headerLength = 33 + 32 * fieldColumns.Length;
            _numFields = fieldColumns.Length;
            _recordLength = tempLength;
        }

        /// <summary>
        /// Remove a column from this DbaseFileHeader.
        /// </summary>
        /// <param name="fieldName"></param>
        /// <returns>return index of the removed column, -1 if no found.</returns>
        public int RemoveColumn(string fieldName)
        {
            int retCol = -1;
            int tempLength = 1;
            var columns =  new DbfColumn[fieldColumns.Length - 1];
            for (int i = 0, j = 0; i < fieldColumns.Length; i++)
            {
                if (fieldName.ToLower() != (fieldColumns[i].Name.Trim().ToLower()))
                {
                    // if this is the last field and we still haven't found the
                    // named field
                    if (i == j && i == fieldColumns.Length - 1)
                        return retCol;
                    columns[j] = fieldColumns[i];
                    columns[j].DataAddress = tempLength;
                    tempLength += columns[j].Length;
                    // only increment j on non-matching fields
                    j++;
                }
                else retCol = i;
            }

            // set the new fields.
            fieldColumns = columns;
            _headerLength = 33 + 32 * fieldColumns.Length;
            _numFields = fieldColumns.Length;
            _recordLength = tempLength;

            return retCol;
        }
   
        private static byte GetLdidFromEncoding(Encoding encoding)
        {
            byte ldid;
            if (!DbaseEncodingUtility.EncodingToLdid.TryGetValue(encoding, out ldid))
                ldid = 0x03;
            return ldid;
        }

        /// <summary>
        /// Set the number of records in the file
        /// </summary>
        /// <param name="inNumRecords"></param>
        protected void SetNumRecords(int inNumRecords)
        {
            _numRecords = inNumRecords;
        }

    }

    internal static class DbaseEncodingUtility
    {
        /// <summary>
        /// The Latin1 Encoding
        /// </summary>
        internal static readonly Encoding Latin1 = GetEncodingForCodePageName("iso-8859-1");

        /// <summary>
        /// The default Encoding
        /// </summary>
        public static Encoding DefaultEncoding { get; set; } = GetEncodingForCodePageIdentifier(1252);

        /// <summary>
        /// Association of language driver id (ldid) to encoding
        /// </summary>
        internal static readonly IDictionary<byte, Encoding> LdidToEncoding;

        /// <summary>
        /// Association of encoding to language driver id (ldid)
        /// </summary>
        internal static readonly IDictionary<Encoding, byte> EncodingToLdid;

        static DbaseEncodingUtility()
        {
            object[][] dbfCodePages =
            {
                new object[] { 0x01 , 437 },  // U.S. MSDOS
                new object[] { 0x02 , 850 },  // International MSDOS
                new object[] { 0x08 , 865 },  // Danish OEM
                new object[] { 0x09 , 437 },  // Dutch OEM
                new object[] { 0x0A , 850 },  // Dutch OEM*
                new object[] { 0x0B , 437 },  // Finnish OEM
                new object[] { 0x0D , 437 },  // French OEM
                new object[] { 0x0E , 850 },  // French OEM*
                new object[] { 0x0F , 437 },  // German OEM
                new object[] { 0x10 , 850 },  // German OEM*
                new object[] { 0x11 , 437 },  // Italian OEM
                new object[] { 0x12 , 850 },  // Italian OEM*
                new object[] { 0x13 , 932 },  // Japanese Shift-JIS
                new object[] { 0x14 , 850 },  // Spanish OEM*
                new object[] { 0x15 , 437 },  // Swedish OEM
                new object[] { 0x16 , 850 },  // Swedish OEM*
                new object[] { 0x17 , 865 },  // Norwegian OEM
                new object[] { 0x18 , 437 },  // Spanish OEM
                new object[] { 0x19 , 437 },  // English OEM (Britain)
                new object[] { 0x1A , 850 },  // English OEM (Britain)*
                new object[] { 0x1B , 437 },  // English OEM (U.S.)
                new object[] { 0x1C , 863 },  // French OEM (Canada)
                new object[] { 0x1D , 850 },  // French OEM*
                new object[] { 0x1F , 852 },  // Czech OEM
                new object[] { 0x22 , 852 },  // Hungarian OEM
                new object[] { 0x23 , 852 },  // Polish OEM
                new object[] { 0x24 , 860 },  // Portuguese OEM
                new object[] { 0x25 , 850 },  // Portuguese OEM*
                new object[] { 0x26 , 866 },  // Russian OEM
                new object[] { 0x37 , 850 },  // English OEM (U.S.)*
                new object[] { 0x40 , 852 },  // Romanian OEM
                new object[] { 0x4D , 936 },  // Chinese GBK (PRC)
                new object[] { 0x4E , 949 },  // Korean (ANSI/OEM)
                new object[] { 0x4F , 950 },  // Chinese Big5 (Taiwan)
                new object[] { 0x50 , 874 },  // Thai (ANSI/OEM)
                new object[] { 0x58 , 1252 }, // Western European ANSI
                new object[] { 0x59 , 1252 }, // Spanish ANSI
                new object[] { 0x64 , 852 },  // Eastern European MSDOS
                new object[] { 0x65 , 866 },  // Russian MSDOS
                new object[] { 0x66 , 865 },  // Nordic MSDOS
                new object[] { 0x67 , 861 },  // Icelandic MSDOS
                new object[] { 0x6A , 737 },  // Greek MSDOS (437G)
                new object[] { 0x6B , 857 },  // Turkish MSDOS
                new object[] { 0x6C , 863 },  // FrenchCanadian MSDOS
                new object[] { 0x78 , 950 },  // Taiwan Big 5
                new object[] { 0x79 , 949 },  // Hangul (Wansung)
                new object[] { 0x7A , 936 },  // PRC GBK
                new object[] { 0x7B , 932 },  // Japanese Shift-JIS
                new object[] { 0x7C , 874 },  // Thai Windows/MSDOS
                new object[] { 0x86 , 737 },  // Greek OEM
                new object[] { 0x87 , 852 },  // Slovenian OEM
                new object[] { 0x88 , 857 },  // Turkish OEM
                new object[] { 0xC8 , 1250 }, // Eastern European Windows
                new object[] { 0xC9 , 1251 }, // Russian Windows
                new object[] { 0xCA , 1254 }, // Turkish Windows
                new object[] { 0xCB , 1253 }, // Greek Windows
                new object[] { 0xCC , 1257 }  // Baltic Windows
            };

            LdidToEncoding = new Dictionary<byte, Encoding>();
            EncodingToLdid = new Dictionary<Encoding, byte>();

            // Register predefined language driver codes with their respective encodings
            RegisterEncodings(dbfCodePages);

            // Add ANSI values 3 and 0x57 as system's default encoding, and 0 which means no encoding.
            AddLdidEncodingPair(0, Encoding.UTF8);
            AddLdidEncodingPair(0x03, Encoding.Default);
            AddLdidEncodingPair(0x57, Encoding.Default);
        }

        public static Encoding GetEncodingForCodePageIdentifier(int codePage) => Encoding.GetEncoding(codePage); 
        public static Encoding GetEncodingForCodePageName(string name) =>  Encoding.GetEncoding(name);

        /*
        private static void AddLdidEncodingPair(byte ldid, int codePage)
        {
            Encoding encToAdd;
            if (!TryGetEncoding("windows-" + codePage, out encToAdd) &&
                !TryGetEncoding(codePage, out encToAdd))
                return;
            AddLdidEncodingPair(ldid, encToAdd);
        }
        */

        private static void AddLdidEncodingPair(byte ldid, Encoding encodingToAdd)
        {
            LdidToEncoding.Add(ldid, encodingToAdd);
            if (!EncodingToLdid.ContainsKey(encodingToAdd))
                EncodingToLdid.Add(encodingToAdd, ldid);
        }

        private static void RegisterEncodings(object[][] ldidCodePagePairs)
        {
            foreach (object[] ldidCodePagePair in ldidCodePagePairs)
            {
                byte ldid = Convert.ToByte(ldidCodePagePair[0], CultureInfo.InvariantCulture);
                int codePage = (int)ldidCodePagePair[1];

                try
                {
                    var enc = GetEncodingForCodePageIdentifier(codePage);
                    AddLdidEncodingPair(ldid, enc);
                }
                catch (NotSupportedException)
                {

                }
            }
        }

    }
}
